/*  ----------------------------------------------------------------------------
 *  Arello Mobile
 *  Fast 3d Engine
 *  ----------------------------------------------------------------------------
 * Except where otherwise noted, this work is licensed under
 * http://creativecommons.org/licenses/by/3.0
 */

#include "Hardware.h"
#include <WindowImpl.h>
#include <opengl/gl.h>
#include <Gluck.h>
#include <logs/Log.h>

#include <QuickTime/Movies.h>

#include "VertexBuffer.h"
#include "Texture.h"

namespace liba
{
namespace lib3d
{
namespace hard
{
namespace macx
{

class MatrixGL
{
	GLfloat data[16];
public:
	MatrixGL( const Matrix & mat )
	{
		for( int y = 0; y < 4; ++y )
			for( int x = 0; x < 4; ++x )
				data[ x*4 + y ] = mat.cr( x, y );
	}
	void glLoad()const
	{
		glLoadMatrixf( data );
	}
	void glMult()const
	{
		glMultMatrixf( data );
	}
};

Hardware::Hardware()
:   context( 0 ),
	dsp_context( 0 ),
	window( 0 ),
	new_window( 0 ),
	fullscreen( false )
{
	Strings names;
	names.push_back("Fullscreen");
	names.push_back("ResolutionX");
	names.push_back("ResolutionY");
//	names.push_back("Antialiasing");
	Strings values;
	values.push_back("Yes");
	values.push_back("800");
	values.push_back("600");
//	values.push_back(String() << "None" );

	set_attribute_names( names, values );
	
	Strings table_row( 3 );
	table_row[0] = "No";
	table_row[1] = "0";
	table_row[2] = "0";
	add_attribute_table_row( table_row );

	OSStatus err;
	err = DSpStartup();
	
	DSpContextReference context;
	DSpContextAttributes attributes;
	
	DisplayIDType display;
	GDHandle main_device = GetMainDevice();
	DMGetDisplayIDByGDevice( main_device, &display, true );

	
	err = DSpGetFirstContext( display, &context );
	
	if( err != noErr )
		throw Gluck( __FUNCTION__, "DSprocket fails" );
		
	if( DSpContext_GetAttributes( context, &attributes ) != noErr )
		throw Gluck( __FUNCTION__, "DSprocket fails" );
	
	bool quit = false;
	while( !quit )
	{
		int width = attributes.displayWidth;
		int height = attributes.displayHeight;
		int depth = attributes.displayBestDepth;
		
		Strings table_row( 3 );
		table_row[0] = "Yes";
		table_row[1] << width;
		table_row[2] << height;
 		add_attribute_table_row( table_row );
		
		table_row[0] = "No";
 		add_attribute_table_row( table_row );
		
		while( width == attributes.displayWidth && height == attributes.displayHeight )
		{
			OSStatus err = DSpGetNextContext( context, &context );
			if( err != noErr )
			{
				quit = true;
				break;
			}
			if( DSpContext_GetAttributes( context, &attributes ) == noErr )
			{
				int depth = attributes.displayBestDepth;
			}
		}
	}
}

Hardware::~Hardware()
{
	stop_mode();
	DSpShutdown();
}

bool Hardware::set_mode_internal(WindowImpl * window, const Strings & attribute_values)
{
	this->window = window;
	
	bool full_screen = attribute_values[0] == "Yes"; 
	int width;
	int height;
	if( !converter::convert( attribute_values[1], &width ) )
		return false;
	if( !converter::convert( attribute_values[2], &height ) )
		return false;

	if( full_screen == true )
	{
/*		memset (&attrib, 0, sizeof (DSpContextAttributes));
		attrib.displayWidth         = width;
		attrib.displayHeight        = height;
		attrib.displayBestDepth     = 32;
		attrib.backBufferBestDepth  = 32;
		attrib.displayDepthMask     = kDSpDepthMask_16 | kDSpDepthMask_32;
		attrib.backBufferDepthMask  = kDSpDepthMask_16 | kDSpDepthMask_32;
		attrib.colorNeeds           = kDSpColorNeeds_Require;
		attrib.colorTable           = 0;
		attrib.pageCount            = 1;
        
        if ( DSpFindBestContext (&attrib, &dsp_context) != noErr )
		{
            logs::log() << "DrawSprocket couldn't find a context\r\n";
            return false;
        }
        
		if ( DSpContext_Reserve (dsp_context, &attrib) != noErr )
		{
			logs::log() << "DrawSprocket couldn't get the needed resources to build the display\r\n";
			return false;
		}
		
		DSpContext_FadeGammaOut( 0, 0 );
	
		if ( DSpContext_SetState (dsp_context, kDSpContextState_Active) != noErr )
		{
			logs::log() << "DrawSprocket couldn't get the needed resources to activate the display\r\n";
			return false;
		}
		
		DSpContext_FadeGammaIn( 0, 0 );
		
		this->fullscreen = true;
		this->width = width;
		this->height = height;

		//it's strange but we have to create new window again for fullscreen mode
		Rect rect;
		RGBColor rgb = { 0.0, 0.0, 0.0 };
		GrafPtr save_port;
	   
		SetRect (&rect, 0, 0, width, height);
		new_window = NewCWindow( 0, &rect, "\p", true, plainDBox, (WindowPtr)-1, false, 0);
		  
		   */
		/* Set window color to black to avoid white flash*/
/*		GetPort (&save_port);
		SetPort ( GetWindowPort( new_window ));
		RGBForeColor (&rgb);
		PaintRect    (&rect);	
		SetPort (save_port);
*/	   
		this->fullscreen = true;
		short qt_width = width;
		short qt_height = height;
		BeginFullScreen( &fullscreen_ctx, 0, &qt_width, &qt_height, &new_window, 0, 0);
		
		WindowRef temp_wnd = window->get_wnd();
		window->get_wnd_bug() = new_window;

		SetPortWindowPort ( new_window );
		SelectWindow  ( new_window );
		new_window = temp_wnd;
	}
	else
	{
		this->fullscreen = false;
		GetWindowPortBounds( window->get_wnd(), &bounds );
		
		ShowWindow( window->get_wnd() );
		SelectWindow( window->get_wnd() );
	}
		
	int i = 0;
	GLint attributes[ 24 ];
	attributes[i++] = AGL_RGBA;
	attributes[i++] = AGL_RED_SIZE;
	attributes[i++] = 8;
	attributes[i++] = AGL_GREEN_SIZE;
	attributes[i++] = 8;
	attributes[i++] = AGL_BLUE_SIZE;
	attributes[i++] = 8;
	attributes[i++] = AGL_ALPHA_SIZE;
	attributes[i++] = 8;

	attributes[i++] = AGL_DOUBLEBUFFER;

	attributes[i++] = AGL_DEPTH_SIZE;
	attributes[i++] = 32;

	attributes[i++] = AGL_ALL_RENDERERS;
	attributes[i++] = AGL_NONE;

	AGLPixelFormat format = aglChoosePixelFormat( 0, 0, attributes );
	context = aglCreateContext( format, 0 );
	
	aglDestroyPixelFormat( format );
	int noerror = aglSetDrawable( context, GetWindowPort( window->get_wnd() ) );
	if( !noerror )
	{
		logs::log() << "Agl failed to initialize\r\n";
		return false;
	}
	
	bool retval = aglSetCurrentContext( context );
	if( !retval )
	{
		logs::log() << "Agl failed to set context\r\n";
		return false;
	}
	
	return true;
}

void Hardware::stop_mode()
{
	if( context )
	{
		aglSetCurrentContext( 0 );
		aglSetDrawable( context, 0 );
		aglDestroyContext( context );
		context = 0;
	}
	
	if( new_window )
	{
		window->get_wnd_bug() = new_window;
		EndFullScreen(fullscreen_ctx, 0);
		new_window = 0;
	}
	
	if( dsp_context )
	{
		DSpContext_FadeGammaOut( 0, 0 );
		DSpContext_SetState( dsp_context, kDSpContextState_Inactive );
		DSpContext_FadeGammaIn( 0, 0 );
		DSpContext_Release( dsp_context );
		dsp_context = 0;
	}
}

hard::Texture * Hardware::create_texture(int mipmap_levels, const pictureformat::Format & desired_format, int width, int height)
{
	GLint max_texture_size = 0;
	glGetIntegerv( GL_MAX_TEXTURE_SIZE, &max_texture_size );
	return new Texture( this, mipmap_levels, pictureformat::R8G8B8A8, width, height, max_texture_size );
}

hard::VertexBuffer * Hardware::create_vertex_buffer( bool dynamic )
{
	return new VertexBuffer( this, dynamic );
}

bool Hardware::start_frame()
{
	return true;
}

void Hardware::flip()
{
	glFlush();
	aglSwapBuffers( context );
}

bool Hardware::start_render( const RenderList & render_list, RenderTarget * render_target )
{
	Viewport vp = render_list.viewport;

//	if( !fullscreen )
	{
		Rect new_bounds;
		GetWindowPortBounds( window->get_wnd(), &new_bounds );
		if( bounds.left != new_bounds.left || bounds.right != new_bounds.right ||
			bounds.top != new_bounds.top || bounds.bottom != new_bounds.bottom )
		{
			bounds = new_bounds;
			aglSetCurrentContext( context );
			aglUpdateContext( context );
//			glViewport( 0, 0, bounds.right - bounds.left, bounds.bottom - bounds.top);
		}
	}
/*	else
	{
//		HideWindow( window->get_wnd() );
//		glViewport( 0, 0, width, height);
	}
*/

	int le = (bounds.right - bounds.left) * (vp.left + 1)/2;
	int ri = (bounds.right - bounds.left) * (vp.right + 1)/2;
	int bo = (bounds.bottom - bounds.top) * ((vp.top + 1)/2);
	int to = (bounds.bottom - bounds.top) * ((vp.bottom + 1)/2);

	glViewport( le, to, ri - le, bo - to);


	glDepthMask( true );
	if( render_list.clear_render_target )
	{
		glClearColor( 0, 0, 0, 0 );
		glClearDepth( 1 );
		glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
	}
	else
	{
		glClearDepth( 1 );
		glClear( GL_DEPTH_BUFFER_BIT );
	}

	glShadeModel( GL_SMOOTH );
	glEnable( GL_LIGHTING );
	glEnable( GL_CULL_FACE );
	glEnable( GL_DEPTH_TEST );
	glEnable( GL_TEXTURE_2D );

	glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR ); 
	glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST );

	glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
	glEnable( GL_BLEND );
	
	glTexEnvi( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE );

	glEnableClientState( GL_VERTEX_ARRAY );
	glEnableClientState( GL_TEXTURE_COORD_ARRAY );

	return true;
}

static void set_lights( RenderList * render_list )
{
	GLint max_lights = 8;
	glGetIntegerv( GL_MAX_LIGHTS, &max_lights );

	int i = 0;
	RenderLights::const_iterator lit = render_list->lights.begin();
	for( ; lit != render_list->lights.end(); ++lit )
	{
		int light_number = GL_LIGHT0 + i;
		
		GLfloat ambient[] = { 0, 0, 0, 1 };
		glLightfv( light_number, GL_AMBIENT, ambient );
		glLightfv( light_number, GL_SPECULAR, ambient );
		
		if( !lit->is_directional )
		{
			GLfloat position[] = { lit->world_position.x, lit->world_position.y, lit->world_position.z, 1 };
			glLightfv( light_number, GL_POSITION, position );
			
			GLfloat diffuse[] = { lit->diffuse.r, lit->diffuse.g, lit->diffuse.b, 1 };
			glLightfv( light_number, GL_DIFFUSE, diffuse );
			
			glLightf( light_number, GL_CONSTANT_ATTENUATION, lit->clq.x/lit->intensity );
			glLightf( light_number, GL_LINEAR_ATTENUATION, lit->clq.y/lit->intensity );
			glLightf( light_number, GL_QUADRATIC_ATTENUATION, lit->clq.z/lit->intensity );
		}
		else
		{
			GLfloat direction[] = { lit->world_direction.x, lit->world_direction.y, lit->world_direction.z, 0 };
			glLightfv( light_number, GL_POSITION, direction );
			
			GLfloat diffuse[] = { lit->diffuse.r * lit->intensity, lit->diffuse.g * lit->intensity, lit->diffuse.b * lit->intensity, 1 };
			glLightfv( light_number, GL_DIFFUSE, diffuse );
		}
		
		glEnable( light_number );
		++i;
		if( i >= max_lights )
			break;
	}
	
	while( i < max_lights )
	{
		int light_number = GL_LIGHT0 + i;
		glDisable( light_number );
		++i;
	}
}

static void ren_model( const RenderModel & render_model )
{
	if( render_model.ib )
		render_model.vb->render( render_model.ib, render_model.primitive_start, render_model.primitive_count, render_model.vertex_start, render_model.vertex_count );
	else
		render_model.vb->render( render_model.primitive_start, render_model.primitive_count );
}

static void ren_model( const RenderSprite & render_model )
{
	render_model.vb->render( render_model.primitive_start, render_model.primitive_count );
}

static void con_tex_ren_model_diffuse_reflecion_emissive( const RenderList & render_list, const RenderModel & render_model )
{
	TextureBase * ref = render_model.material.cubic_reflection_texture.texture;
	TextureBase * dif = render_model.material.diffuse_texture.texture;
	TextureBase * emi = render_model.material.emissive_texture.texture;

	//simplest style, no multitexturing
	if( !emi )
	{
		if( dif )
			dif->set_in_hardware( 0 );
		else
			glBindTexture( GL_TEXTURE_2D, 0 );

		ren_model( render_model );
	}
	else
	{
		emi->set_in_hardware( 0 );

		GLfloat emissive[4] = { render_model.material.emissive.r,
							render_model.material.emissive.g,
							render_model.material.emissive.b,
							0 };

		GLfloat diffuse[4];
		glGetMaterialfv( GL_FRONT, GL_DIFFUSE, (GLfloat*)&diffuse );
		diffuse[0] = diffuse[1] = diffuse[2] = 0;

		glMaterialfv( GL_FRONT, GL_DIFFUSE, diffuse );
		glMaterialfv( GL_FRONT, GL_EMISSION, emissive );

		glBlendFunc( GL_ONE, GL_ONE_MINUS_SRC_ALPHA );
		ren_model( render_model );
		glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
	}
}

static void con_tex_ren_model_diffuse_reflecion_emissive( const RenderList & render_list, const RenderSprite & render_model )
{
	TextureBase * ref = render_model.material.cubic_reflection_texture.texture;
	TextureBase * dif = render_model.material.diffuse_texture.texture;
	TextureBase * emi = render_model.material.emissive_texture.texture;

	//simplest style, no multitexturing
	if( !emi )
	{
		if( dif )
			dif->set_in_hardware( 0 );
		else
			glBindTexture( GL_TEXTURE_2D, 0 );

		ren_model( render_model );
	}
	else
	{
		emi->set_in_hardware( 0 );

		GLfloat emissive[4] = { render_model.material.emissive.r,
							render_model.material.emissive.g,
							render_model.material.emissive.b,
							0 };

		GLfloat diffuse[4];
		glGetMaterialfv( GL_FRONT, GL_DIFFUSE, (GLfloat*)&diffuse );
		diffuse[0] = diffuse[1] = diffuse[2] = 0;

		glMaterialfv( GL_FRONT, GL_DIFFUSE, diffuse );
		glMaterialfv( GL_FRONT, GL_EMISSION, emissive );

		glBlendFunc( GL_ONE, GL_ONE_MINUS_SRC_ALPHA );
		ren_model( render_model );
		glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
	}
}

static void prepare_model( const Matrix & world, const RenderMaterial & material, bool is_opaque )
{
	glPopMatrix();
	glPushMatrix();
	MatrixGL mat(world);
	mat.glMult();
//	glMultMatrixf( (float *)&world );
	
	const double opacity = 1 - material.transparency;
	
	//see directx driver for details
	GLfloat diffuse[] = { material.diffuse.r * opacity,
						material.diffuse.g * opacity,
                        material.diffuse.b * opacity,
						opacity };
	glMaterialfv( GL_FRONT, GL_DIFFUSE, diffuse );
	diffuse[3] = 0;
	glMaterialfv( GL_FRONT, GL_AMBIENT, diffuse );
	GLfloat emissive[] = { 0, 0, 0, 0 };
	glMaterialfv( GL_FRONT, GL_EMISSION, emissive );
}

static void set_z_less_write()
{
	glDepthFunc( GL_LESS );
	glDepthMask( true );
}

static void set_z_lessequal_nowrite()
{
	glDepthFunc( GL_LEQUAL );
	glDepthMask( false );
}

static void set_alpha_test_keep_only_1()
{
	glAlphaFunc( GL_EQUAL, 0xFF );
	glEnable( GL_ALPHA_TEST );
}

static void set_alpha_test_off()
{
	glDisable( GL_ALPHA_TEST );
}

void Hardware::render( RenderList * rl, RenderTarget * render_target )
{
	const RenderList & render_list = *rl;

	if( !start_render( render_list, render_target ) )
		return;

	rl->calculate( 0, 0 );

	glMatrixMode( GL_PROJECTION );
	glLoadIdentity();
	MatrixGL mat(rl->camera_projection);
	mat.glMult();

	glMatrixMode( GL_MODELVIEW );
	mat = MatrixGL(rl->view_mat);
	mat.glLoad();

	set_lights( rl );
	
	GLfloat global_ambient[] = { rl->ambient_color.r, rl->ambient_color.g, rl->ambient_color.b, 1 };
	glLightModelfv( GL_LIGHT_MODEL_AMBIENT, global_ambient );
	
	glPushMatrix();
	
	glCullFace( GL_FRONT );
	set_z_less_write();
//	set_alpha_test_keep_only_1();
	{
		RenderOrder::const_iterator cit = render_list.render_order.begin();
		for( ; cit != render_list.render_order.begin() + render_list.first_transparent_model; ++cit )
		{
			const RenderModel * model = &render_list.models[cit->number];
			prepare_model( model->world, model->material, true );
			con_tex_ren_model_diffuse_reflecion_emissive( render_list, *model );
		}
	}
	
	glEnable( GL_BLEND );

//	set_alpha_test_off();
	set_z_lessequal_nowrite();
	{
		RenderOrder::const_iterator cit = render_list.render_order.begin() + render_list.first_transparent_model;
		for( ; cit != render_list.render_order.end(); ++cit )
		{
			if(cit->is_sprite)
			{
				const RenderSprite * sprite = &render_list.sprites[cit->number];
				prepare_model( sprite->world, sprite->material, false );
				con_tex_ren_model_diffuse_reflecion_emissive( render_list, *sprite );
				}
			else
			{
				const RenderModel * model = &render_list.models[cit->number];
				prepare_model( model->world, model->material, false );
				con_tex_ren_model_diffuse_reflecion_emissive( render_list, *model );
		}
		}
	}

	glPopMatrix();
}

}
}
}
}